6장. HLS의 한계를 줄이기 위한 변화 — LL-HLS
6.1 문제 재정의
5장에서 우리는 지연의 원인을 정리했다.
Latency ≈ encoder + segment + polling + buffer
LL-HLS(Low-Latency HLS)는 이 중 인코더를 제외한 세 가지를 모두 손본 기술이다.
| 요소 | 기존 HLS | LL-HLS |
|---|---|---|
| Segment | 완성 후 전송 | 생성 중 전송 (Part) |
| Polling | 일정 주기 | 대기 + 즉시 응답 (Blocking) |
| Buffer | 6~10초 | 1~3초 |
| 미래 정보 | 없음 | Preload Hint로 미리 알림 |
LL-HLS는 2019년 Apple이 발표하고, 2020년 HLS 명세에 정식 편입되었다.
6.2 첫 번째 시도: Segment를 더 작게
가장 단순한 접근부터 보자.
기존 HLS에서는 보통 2~10초 단위로 segment를 만든다.
“그럼 segment를 작게 만들면 되지 않나?”
- 2초 → 1초로 줄이면 → segment 대기 1초 감소
- 0.5초로 줄이면 → 더 감소
이렇게 해도 일부 효과는 있다.
하지만 한계가 있다.
작게 만들기의 한계
- 파일 수 폭증 → HTTP 요청 부담
- 매 segment마다 keyframe → 화질/용량 손실
- CDN 캐싱 효율 저하
- playlist 자체가 거대해짐
무엇보다 중요한 건 이거다.
여전히 “완성된 뒤에 전송한다“는 구조는 그대로다.
근본 문제가 안 풀린다.
LL-HLS는 더 본질적인 변화를 시도한다.
6.3 두 번째 변화: 생성 중 전송
여기서 HLS 구조를 크게 바꾸는 아이디어가 나온다.
segment가 완성될 때까지 기다리지 말고, 만드는 중간부터 전달하자
기존 방식:
0~2초 데이터 수집 → seg1 완성 → 전송 시작
↑ 2초 대기
LL-HLS:
0~0.2초 → 즉시 전달 (part 1)
0.2~0.4초 → 즉시 전달 (part 2)
0.4~0.6초 → 즉시 전달 (part 3)
...
2초까지 → segment 완성 (마지막 part 포함)
이 변화의 핵심 표현:
“완성 후 전송“에서 “생성 중 전송“으로
6.3.1 Segment와 Part의 역할 구분
여기서 헷갈리기 쉬운 부분을 정리하자.
- Segment: 플레이어가 사용하는 재생 단위
- Part: 그 안을 더 잘게 나눈 전송 단위
하나의 segment는 여러 part로 구성된다.
seg100 ┐
├── part 0 (0.0~0.2초)
├── part 1 (0.2~0.4초)
├── part 2 (0.4~0.6초)
...
└── part 9 (1.8~2.0초)
플레이어는
- 일반 시청자: segment를 받아서 재생
- 라이브 최전선: part를 받아서 즉시 재생
이걸 동시에 지원한다.
(공식 용어로 part는 종종 chunk와 혼용되지만, HLS 명세는 Part라는 용어를 쓴다. DASH/CMAF에서는 chunk다.)
6.3.2 EXT-X-PART
각 part는 m3u8에
#EXT-X-PART 태그로 선언된다.
#EXT-X-PART:DURATION=0.200,URI="seg100.0.m4s"
#EXT-X-PART:DURATION=0.200,URI="seg100.1.m4s"
#EXT-X-PART:DURATION=0.200,URI="seg100.2.m4s"
#EXT-X-PART:DURATION=0.200,URI="seg100.3.m4s"
#EXT-X-PART:DURATION=0.200,URI="seg100.4.m4s"
#EXT-X-PART:DURATION=0.200,URI="seg100.5.m4s"
#EXT-X-PART:DURATION=0.200,URI="seg100.6.m4s"
#EXT-X-PART:DURATION=0.200,URI="seg100.7.m4s"
#EXT-X-PART:DURATION=0.200,URI="seg100.8.m4s",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.200,URI="seg100.9.m4s"
#EXTINF:2.000,
seg100.m4s
주요 속성:
| 속성 | 의미 |
|---|---|
| URI | part 파일 위치 |
| DURATION | part 길이 (초) |
| INDEPENDENT | 자기 자신만으로 디코딩 가능한가 (keyframe 시작) |
| BYTERANGE | 큰 파일에서 부분만 가져오는 경우 |
| GAP | 누락된 part 표시 |
INDEPENDENT=YES인 part는
ABR 전환 지점으로 쓸 수 있다.
6.3.3 EXT-X-PART-INF
#EXT-X-PART-INF:PART-TARGET=0.200
이 playlist의 part 기본 길이.
플레이어는 이 값을 보고 polling 간격이나 버퍼 크기를 조정한다.
6.4 세 번째 변화: Polling에서 Blocking으로
5장에서 polling 지연을 다뤘다.
플레이어가 일정 주기로 묻기 때문에 새 데이터가 즉시 도착하지 않는다.
LL-HLS는 이 구조를 바꾼다.
기존: 요청 → 응답 → 종료 → 다시 요청
LL-HLS: 요청 → (서버가 기다림) → 새 데이터 생기면 즉시 응답
이를 Blocking Playlist Reload라고 한다.
6.4.1 EXT-X-SERVER-CONTROL
이 동작을 가능하게 하는 핵심 태그.
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,
PART-HOLD-BACK=0.6,
HOLD-BACK=6.0,
CAN-SKIP-UNTIL=12.0
서버가 클라이언트에게 “나는 이런 동작을 지원한다“고 알리는 태그다.
6.4.2 주요 속성
CAN-BLOCK-RELOAD=YES
서버가 blocking reload를 지원한다는 선언.
이게 있어야 클라이언트가 “새 데이터 나올 때까지 기다려“라고 요청할 수 있다.
HOLD-BACK
플레이어가 라이브 최전선에서 얼마만큼 떨어진 지점에서 재생해야 하는가.
HOLD-BACK=6.0 → 최신에서 6초 뒤에서 재생
이 값이 클수록 안정적이지만 지연이 늘어난다.
LL-HLS에서는 더 짧은 버퍼를 위해 PART-HOLD-BACK도 함께 쓴다.
PART-HOLD-BACK
part 단위 재생 시 최전선에서 얼마나 떨어질지.
PART-HOLD-BACK=0.6 → 최신 part에서 0.6초 뒤
보통 PART-TARGET × 3 정도 값을 쓴다.
CAN-SKIP-UNTIL
playlist 크기를 줄이기 위한 기능.
GET /playlist.m3u8?_HLS_skip=YES
오래된 segment 정보를 생략하고 최근 데이터만 전송한다.
playlist가 거대해지는 라이브 환경에서 유용하다.
6.4.3 Blocking Reload (_HLS_msn, _HLS_part)
플레이어는 특별한 쿼리 파라미터로 blocking을 요청한다.
GET /index.m3u8?_HLS_msn=101&_HLS_part=3
의미는 이렇다.
“media-sequence 101의 part 3이 playlist에 추가될 때까지 응답을 미뤄줘”
서버는 그 시점이 올 때까지 HTTP 응답을 보내지 않고 대기한다.
- 새 데이터가 생기면 → 즉시 응답
- 너무 오래 걸리면 → 타임아웃
플레이어는 응답을 받자마자 다음 part를 요청한다.
6.4.4 흐름 비교: Polling vs Blocking
기존 polling:
T=0.0 요청 → 응답 (데이터 없음)
T=1.0 요청 → 응답 (데이터 없음)
T=2.0 요청 → 응답 (seg100 추가!) ← 1~2초 지연
LL-HLS blocking:
T=0.0 요청 (대기 시작)
T=2.0 서버에서 seg100 생성됨 → 즉시 응답
플레이어: 다시 요청 (대기 시작)
T=2.2 part 1 생성 → 즉시 응답
T=2.4 part 2 생성 → 즉시 응답
차이는 분명하다.
정해진 주기로 묻지 않고, 새 데이터가 나오는 순간 받는다.
6.5 미래를 미리 알려주기
LL-HLS는 또 다른 카드를 꺼낸다.
“다음에 무엇이 올지 미리 알려주자”
6.5.1 EXT-X-PRELOAD-HINT
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="seg101.0.m4s"
의미:
이 part는 아직 만들어지지 않았지만, 다음에 이 URL로 만들어질 것이다.
플레이어는 이 URL로 선행 요청을 보낼 수 있다.
서버는 데이터가 만들어지는 즉시 응답에 흘려보낸다. (HTTP Chunked Transfer로)
이를 통해 polling 자체를 건너뛴다.
6.5.2 EXT-X-RENDITION-REPORT
ABR을 위한 힌트.
각 화질의 m3u8에 들어간다.
#EXT-X-RENDITION-REPORT:URI="../720p/index.m3u8",
LAST-MSN=101,
LAST-PART=2
의미:
“현재 화질을 1080p로 보고 있다면, 720p의 최신 상태는 media-sequence=101, part=2다”
플레이어가 화질을 바꿀 때 다시 처음부터 따라잡을 필요가 없다.
이전에는 720p로 전환하려면:
- 720p playlist 요청
- 어디까지 왔는지 파악
- 따라잡기 위해 시간 소요
LL-HLS에서는:
- 미리 알려준 위치로 즉시 점프
6.6 HTTP Chunked Transfer Encoding
LL-HLS의 backbone 기술이다.
데이터가 다 만들어지기 전에 응답을 시작하고, 만들어지는 대로 전송한다.
일반 HTTP 응답
HTTP/1.1 200 OK
Content-Length: 1024000
[1024000 bytes 모두 한 번에]
서버는 데이터를 다 만든 뒤 보낸다.
Chunked Transfer
HTTP/1.1 200 OK
Transfer-Encoding: chunked
200\r\n
[512 bytes]\r\n ← 1차 chunk
200\r\n
[512 bytes]\r\n ← 2차 chunk
...
0\r\n\r\n ← 종료
서버는 chunk 단위로 데이터를 흘려보낸다.
- Content-Length 불필요
- 응답이 시작되어도 끝이 정해지지 않음
- 클라이언트는 받자마자 처리
LL-HLS와의 결합
T=0.0 서버: 응답 시작 (헤더 전송)
T=0.2 서버: part 1 chunk 전송 → 플레이어가 즉시 디코딩
T=0.4 서버: part 2 chunk 전송 → 즉시 디코딩
...
T=2.0 서버: 마지막 chunk + 종료
이 구조 덕분에 LL-HLS는 TCP 연결 하나로 part들이 흘러나오는 경험을 만든다.
push가 아닌데 push처럼 보인다.
6.7 LL-HLS의 한계
지금까지의 변화를 정리하면 이렇다.
LL-HLS는 세 방향으로 개선했다.
| 변화 | 방법 |
|---|---|
| Segment 대기 | Part로 잘게 나눠 즉시 전송 |
| Playlist polling | Blocking Reload |
| Player buffer | Part 단위로 줄임 |
| 전환 지연 | Preload Hint / Rendition Report |
지연은 다음 수준으로 줄어든다.
| 구분 | 일반적 지연 |
|---|---|
| 기존 HLS | 6~10초 |
| LL-HLS | 2~3초 |
| 잘 튜닝된 LL-HLS | 1~2초 |
한계
하지만 중요한 사실이 남아 있다.
구조 자체는 여전히 HLS다.
다음 요소는 그대로 유지된다.
- HTTP 요청 기반
- playlist 중심 동작
- player buffer 필요
- CDN 캐싱 모델
그래서 1초 미만의 초저지연은 LL-HLS의 영역이 아니다.
- 1~3초 지연 → LL-HLS / LL-DASH
- 200~500ms 지연 → WebRTC
- 비교: 일반 HLS → 6~10초
6.8 한 문장 정리
LL-HLS는 HLS의 정체성을 지키면서 지연을 현실적으로 줄이기 위한 개선이다.
다음 장에서는 이 모든 변화가 가능하게 한 데이터 구조의 변화 — fMP4와 CMAF를 다룬다.
6장 한 줄 정리
LL-HLS는 “기다림이 있던 모든 자리“에 부분 전송과 즉시 응답을 끼워 넣은 결과다.